feat(image): convert HEIC paste uploads to JPEG#151
Merged
Conversation
When a browser pastes an HEIC file without normalising it first, the paste-image route now converts it to JPEG server-side via heic-convert before writing to .claude-images/. Magic-byte validation confirms the output is valid JPEG. Adds type declarations for the heic-convert package. Co-authored-by: Saqeb Akhter <saqeb.akhter@gmail.com>
…cy cap, and magic-byte routing (PR Ark0N#151) - Event-loop blockage: HEIC decode/encode (CPU-synchronous libheif WASM + jpeg-js) now runs in a per-conversion worker_threads Worker (src/web/heic-jpeg-worker.ts, spawned by heic-jpeg-converter.ts) with resourceLimits and a 30s hard timeout that terminates the worker — verified end-to-end under tsx and against compiled dist/ output with a real iPhone HEIC (event-loop max stall 52ms during conversion). - No server-side concurrency cap: conversions now acquire a slot from the existing global runWithConversionLimit() pool (document-conversion-limiter), bounding peak decode memory/CPU across simultaneous uploads. - Decompression bomb: header-declared dimensions are read via heic-decode's allocation-free `.all` path and rejected above 64MP BEFORE decode() can allocate width*height*4 bytes (a <300-byte crafted file can declare 30000x30000 = 3.6GB). Regression-tested with a crafted ISOBMFF fixture against the real heic-decode WASM (test/heic-jpeg-core.test.ts). - Mislabeled HEIC (documented Android/MIUI case): conversion now routes on ftyp magic-byte sniff of the raw buffer regardless of declared ext/Content-Type, so a HEIF uploaded as image/jpeg converts instead of 415ing; the magic-mismatch 415 only fires for genuinely unrecognized bytes. - Brand allowlist narrowed to what heic-decode's isHeic() accepts (heim/heis/hevm/hevs dropped — they could only ever fail conversion). - Converted-output size: the JPEG result is checked against MAX_PASTE_IMAGE_BYTES (jpeg-js can inflate a within-limit HEIC past the cap). - Deps: heic-convert replaced with its underlying heic-decode + jpeg-js (the wrapper could not expose the pre-decode dimension check); lockfile synced, drops pngjs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Owner
|
Merged — thank you @aakhter! HEIC→JPEG conversion removes a real iPhone paste-flow papercut. Review moved the decode off the main thread (a worker with resourceLimits + timeout — synchronous WASM decode was freezing every session's streaming for seconds per photo), capped concurrency via the existing conversion limiter, added a 64MP pre-decode guard (a <1MB crafted header could force a multi-GB allocation), and switched detection to magic bytes so mislabeled Android HEIFs convert too. 🙏 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When a browser pastes an HEIC image (common on macOS/iOS when files aren't normalised to JPEG), the paste-image route now converts it server-side to JPEG via
heic-convertbefore writing to.claude-images/.convertHeicToJpeg()helper that importsheic-convertdynamically, validates the output has valid JPEG magic bytes before returningALLOWED_IMAGE_EXTSset (they were already listed in the MIME match but the set was missing them).claude-images/dir setup soextis already.jpgwhen the filename is generatedsrc/types/heic-convert.d.tstype declarations.jpgpathTest plan
npx tsc --noEmit— 0 errorsnpm run lint— cleannpx prettier --check— cleannpm test -- test/routes/session-routes.test.ts— 54/54 passed (including new HEIC test)npm run build— cleanCo-authored-by: Saqeb Akhter saqeb.akhter@gmail.com